package app

import (
	"encoding/json"
	"fmt"
	"net/http"
	"strings"
	"sync"
	"time"

	"github.com/astaxie/beego"
	sdk "github.com/opensourceways/go-gitee/gitee"
	"github.com/opensourceways/server-common-lib/utils"
	"github.com/sirupsen/logrus"
	"k8s.io/apimachinery/pkg/util/sets"

	"cvevulner/cve-ddd/domain"
	"cvevulner/cve-ddd/domain/backend"
	"cvevulner/cve-ddd/domain/bulletin"
	"cvevulner/cve-ddd/domain/majun"
	"cvevulner/cve-ddd/domain/obs"
	"cvevulner/cve-ddd/domain/repository"
	"cvevulner/cve-ddd/domain/testresult"
	"cvevulner/cve-ddd/domain/updateinfo"
)

const (
	EOF = "\n"

	fileIndex       = "index.txt"
	fileUpdateFixed = "update_fixed.txt"
)

type BulletinService interface {
	GenerateBulletins([]string, string) (string, error)
}

func NewBulletinService(
	o obs.OBS,
	r repository.CveRepository,
	m majun.Majun,
	b bulletin.Bulletin,
	t testresult.Result,
	bd backend.Backend,
	l *logrus.Entry,
	u updateinfo.UpdateInfo,
) *bulletinService {
	service := &bulletinService{
		obs:        o,
		repo:       r,
		maJun:      m,
		bulletin:   b,
		testResult: t,
		backend:    bd,
		updateinfo: u,
		log:        l,
		giteeToken: beego.AppConfig.String("gitee::git_token"),
	}

	service.initReleaseDate()

	return service
}

type bulletinService struct {
	obs         obs.OBS
	repo        repository.CveRepository
	maJun       majun.Majun
	bulletin    bulletin.Bulletin
	testResult  testresult.Result
	backend     backend.Backend
	updateinfo  updateinfo.UpdateInfo
	releaseDate sync.Map
	giteeToken  string

	log *logrus.Entry
}

func (b *bulletinService) GenerateBulletins(cveNum []string, date string) (string, error) {
	handleBranch, err := b.maJun.GetReleasedBranch()
	if err != nil {
		return "", fmt.Errorf("get release branch err: %w", err)
	}

	b.log.Infof("handle branch are %v", handleBranch)

	domain.InitMaintainVersion(handleBranch)

	b.log.Infof("need to handle cve are %v", cveNum)
	cves, err := b.repo.FindCves(repository.Option{CveNum: cveNum})
	if err != nil {
		return "", fmt.Errorf("find cves failed: %w", err)
	}

	if len(cves) == 0 {
		return "", fmt.Errorf("no cves")
	}

	if len(cveNum) != len(cves) {
		b.log.Errorf("num of cveNum %d and num of cves %d does not match", len(cveNum), len(cves))
	}

	// 用关联pr并且合入的分支进行过滤
	cves = b.filterByRelatedPR(cves, handleBranch)

	// 用已发布的分支进行过滤
	cves = b.filterByPublishedBranch(cves)

	b.testResult.Init(handleBranch, date)

	// 用成功转测的数据进行过滤，只发布转测成功的cve
	testedCves := b.testResult.Filter(cves)

	indexContent, err := b.getIndexContent()
	if err != nil {
		return "", fmt.Errorf("get %s failed: %w", fileIndex, err)
	}

	maxColdPatchId, err := domain.ParseMaxIdFromIndexTxt(indexContent, domain.BulletinTypeSA)
	if err != nil {
		return "", fmt.Errorf("parse max id failed: %w", err)
	}

	uploadDir := b.generateUploadDir()

	bulletins := testedCves.GenerateBulletins()

	var updateFixedFiles []string

	for _, v := range bulletins {
		maxColdPatchId++

		v.SetIdentificationOfColdPatch(maxColdPatchId)

		v.ProductTree = b.testResult.GenerateProductTree(v.Component, v.AffectedVersion)

		xmlData, err1 := b.bulletin.GenerateColdPatch(&v)
		if err1 != nil {
			b.log.Errorf("generate cold patch %s failed: %v", v.Identification, err1)
			continue
		}

		path := uploadDir + v.CvrfFileName()
		if err1 = b.obs.Upload(path, xmlData); err1 != nil {
			b.log.Errorf("upload cold patch %s failed: %v", v.Identification, err1)
			continue
		}

		updateFixedFiles = append(updateFixedFiles, v.PathAppendToIndexFile())

		b.uploadUpdateInfoFile(&v)
	}

	b.uploadIndexAndFixed(uploadDir, indexContent, updateFixedFiles)

	return uploadDir, nil
}

func (b *bulletinService) uploadUpdateInfoFile(bulletin *domain.SecurityBulletin) {
	for _, branch := range bulletin.AffectedVersion {
		filePath := domain.UpdateinfoRootDir + branch + "/updateinfo.xml"
		downloadBys, err := b.obs.Download(filePath)
		if err != nil {
			b.log.Error(err)
			continue
		}

		data, err := b.updateinfo.UploadUpdateInfoXml(domain.UpdateParam{
			Sb:          bulletin,
			Branch:      branch,
			DownloadBys: downloadBys,
		})

		if err != nil {
			b.log.Error(err)
			continue
		}

		if err = b.obs.Upload(filePath, data); err != nil {
			b.log.Error(err)
			continue
		}
	}
}

func (b *bulletinService) uploadIndexAndFixed(uploadDir, indexContent string, updateFixedFiles []string) {
	updateFixedContent := strings.TrimSpace(strings.Join(updateFixedFiles, EOF))
	newIndexContent := strings.TrimSpace(indexContent) + EOF + updateFixedContent

	indexPath := uploadDir + fileIndex
	updateFixedPath := uploadDir + fileUpdateFixed

	if err := b.obs.Upload(indexPath, []byte(newIndexContent)); err != nil {
		b.log.Errorf("upload %s failed: %v", fileIndex, err)
	}

	if err := b.obs.Upload(updateFixedPath, []byte(updateFixedContent)); err != nil {
		b.log.Errorf("upload %s failed: %v", fileUpdateFixed, err)
	}
}

func (b *bulletinService) generateUploadDir() string {
	parentDir := beego.AppConfig.String("obs::upload_cvrf_dir")

	subDir := time.Now().Format("2006-01-02-15-04-05") + "-new/"

	return parentDir + subDir
}

func (b *bulletinService) getIndexContent() (string, error) {
	cvrfdir := beego.AppConfig.String("obs::download_cvrf_dir")
	content, err := b.obs.Download(cvrfdir + fileIndex)

	return string(content), err
}

func (b *bulletinService) filterByRelatedPR(cves domain.Cves, handleBranch []string) domain.Cves {
	handleBranchSet := sets.NewString(handleBranch...)

	var filteredCves domain.Cves
	for _, v := range cves {
		prs, _, err := b.getRelatedPR(v.ColdIssue)
		if err != nil {
			b.log.Errorf("get related pr of %s err: %s", v.ColdIssue.Number, err.Error())
			continue
		}

		relatedPrSets := sets.NewString()
		for _, pr := range prs {
			if pr.Base.Repo.Namespace.Path != defaultOwner {
				continue
			}

			if pr.State != "merged" {
				continue
			}

			branch := pr.Base.Ref

			releaseTimeOfBranch, ok := b.releaseDate.Load(branch)
			if ok {
				releaseTime := releaseTimeOfBranch.(time.Time)
				mergeAt, err1 := time.ParseInLocation("2006-01-02T15:04:05+08:00", pr.MergedAt, time.Local)
				if err1 != nil {
					b.log.Errorf("parse %s %s %s merge time failed: %s",
						v.CveNum, v.ColdIssue.Number, branch, err1.Error(),
					)
					continue
				}

				if releaseTime.After(mergeAt) {
					b.log.Errorf("release branch time of [%s %s %s] check failed: merge time: %v, release time: %v",
						v.CveNum, v.ColdIssue.Number, branch, mergeAt, releaseTime,
					)
					continue
				}
			}

			relatedPrSets.Insert(branch)
		}

		intersection := handleBranchSet.Intersection(relatedPrSets)
		v.AffectedVersion = intersection.UnsortedList()

		b.log.Infof("the affected version of [%s %s] after pr filter are %v",
			v.CveNum, v.ColdIssue.Number, v.AffectedVersion,
		)

		filteredCves = append(filteredCves, v)
	}

	return filteredCves
}

func (b *bulletinService) getRelatedPR(issue domain.Issue) (prs []sdk.PullRequest, code int, err error) {
	endpoint := fmt.Sprintf("https://gitee.com/api/v5/repos/%v/issues/%v/pull_requests?access_token=%s&repo=%s",
		defaultOwner, issue.Number, b.giteeToken, issue.Repo,
	)
	req, err := http.NewRequest(http.MethodGet, endpoint, nil)
	if err != nil {
		return
	}

	cli := utils.NewHttpClient(3)
	bytes, code, err := cli.Download(req)
	if err != nil {
		return
	}

	err = json.Unmarshal(bytes, &prs)

	return
}

func (b *bulletinService) filterByPublishedBranch(cves domain.Cves) domain.Cves {
	var filteredCves domain.Cves

	for _, v := range cves {
		publishedBranch, err := b.backend.PublishedInfo(v.CveNum, v.ColdIssue.Repo)
		if err != nil {
			b.log.Errorf("filter %s release branch failed: %s", v.CveNum, err.Error())
			continue
		}

		publishSets := sets.New(publishedBranch...)
		affectedVersion := sets.New(v.AffectedVersion...)
		diff := affectedVersion.Difference(publishSets)

		if len(diff) == 0 {
			b.log.Errorf("all branch of %s have been published", v.CveNum)
			continue
		}

		v.AffectedVersion = diff.UnsortedList()

		b.log.Infof("the affected version of [%s %s] after published filter are %v",
			v.CveNum, v.ColdIssue.Number, v.AffectedVersion,
		)

		filteredCves = append(filteredCves, v)
	}

	return filteredCves
}

func (b *bulletinService) initReleaseDate() {
	releaseDateConfig := beego.AppConfig.DefaultString("excel::release_date_of_version", "")
	for _, v := range strings.Split(strings.Trim(releaseDateConfig, ";"), ";") {
		split := strings.Split(v, ":")
		key := split[0]
		value, _ := time.ParseInLocation("2006-01-02", split[1], time.Local)
		b.releaseDate.Store(key, value.AddDate(0, 0, 1))
	}
}
